Flip game II [DP, Hash]

Time: O(N+C^2); Space: O(C); medium

You are playing the following Flip Game with your friend: Given a string that contains only these two characters: + and -,

you and your friend take turns to flip two consecutive “++” into “–”.

The game ends when a person can no longer make a move and therefore the other person will be the winner.

Write a function to determine if the starting player can guarantee a win.

Example 1:

Input: s = “++++”

Output: True

Explanation:

  • The starting player can guarantee a win by flipping the middle “++” to become “+–+”.

Example 2:

Input: s = “+++++”

Output: False

Explanation:

  • The starting player can not win

  • “+++–” –> “+—-”

  • “++–+” –> “—-+”

Follow up:

  • Derive your algorithm’s runtime complexity.

1. DP

[1]:
import re

class Solution1(object):
    def canWin(self, s):
        """
        :type s: str
        :rtype: bool
        """
        g, g_final = [0], 0

        for p in list(map(len, re.split('-+', s))):
            while len(g) <= p:
                # Theorem 2: g[game] = g[subgame1]^g[subgame2]^g[subgame3]...
                # and find first missing number.
                g += min(set(range(p)) - {x^y for x, y in zip(g[:len(g)//2], g[-2:-len(g)//2-2:-1])}),

            g_final ^= g[p]

        return g_final > 0  # Theorem 1: First player must win iff g(current_state) != 0
[3]:
sol = Solution1()
s = "++++"
assert sol.canWin(s) == True

2. Hash

  • We have total O(2^c) game strings,

  • and each hash key in hash table would cost O(c),

  • each one has O(c) choices to the next one,

  • and each one would cost O(clogc) to sort,

So we get O((c * 2^c) * (c * clogc)) = O(c^3 * 2^c * logc) time.

To cache the results of all combinations, thus O(c * 2^c) space.

[4]:
import re

class Solution2(object):
    """
    Hash solution
    Time:  O(n + c^3 * 2^c * logc), n is length of string, c is count of "++"
    Space: O(c * 2^c)
    """
    def canWin(self, s):
        """
        :type s: str
        :rtype: bool
        """
        lookup = {}

        def canWinHelper(consecutives):                                         # O(2^c) time
            consecutives = tuple(sorted(c for c in consecutives if c >= 2))     # O(clogc) time
            if consecutives not in lookup:
                lookup[consecutives] = any(not canWinHelper(consecutives[:i] + (j, c-2-j) + consecutives[i+1:])  # O(c) time
                                           for i, c in enumerate(consecutives)  # O(c) time
                                           for j in range(c - 1))               # O(c) time
            return lookup[consecutives]                                         # O(c) time

        # re.findall: O(n) time, canWinHelper: O(c) in depth
        return canWinHelper(map(len, re.findall(r'\+\++', s)))
[5]:
sol = Solution2()
s = "++++"
assert sol.canWin(s) == True
[6]:
class Solution3(object):
    """
    Time:  O(c * n * c!), n is length of string, c is count of "++"
    Space: O(c * n), recursion would be called at most c in depth.
           Besides, it costs n space for modifying string at each depth.
    """
    def canWin(self, s):
        """
        :type s: str
        :rtype: bool
        """
        i, n = 0, len(s) - 1
        is_win = False
        while not is_win and i < n:                                     # O(n) time
            if s[i] == '+':
                while not is_win and i < n and s[i+1] == '+':           # O(c) time
                     # t(n, c) = c * (t(n, c-1) + n) + n = ...
                     # = c! * t(n, 0) + n * c! * (c + 1) * (1/0! + 1/1! + ... 1/c!)
                     # = n * c! + n * c! * (c + 1) * O(e) = O(c * n * c!)
                    is_win = not self.canWin(s[:i] + '--' + s[i+2:])    # O(n) space
                    i += 1
            i += 1
        return is_win
[7]:
sol = Solution3()
s = "++++"
assert sol.canWin(s) == True